Slay the Spire Clone Godot 4 Tutorial
全部で8動画ある
https://www.youtube.com/watch?v=ulgh_neTJG8
part 1
完成形の共有
初期セットアップ
背景画像の設定
画面サイズの設定
https://www.youtube.com/watch?v=Pa0P1lUoC-M
part2
Card UI と、Card State Machine の基礎作成
非常に参考になったkidooom.icon
カードの State を作成
CardState クラスを作成し、enum と上書きすべきメソッドを用意
CardStateMachine クラスを作成し、CardState の遷移に応じてハンドリングする
BaseState に enter した時に、card_ui を初期化する処理を書く
DraggingState では、ui_layer ノードを group から検索して取得し、reparent(ui_layer)を設定している
カードをクリックして、画面上部の領域で離したらカードを使う
Area2D + CollisionShape2D でカード領域とカードドロップ領域を定義
HBoxContainer で並べるため、カードUI Control ノードには Custom Minimu Size を指定しておく
そうするとそのサイズを考慮して横に並べてくれる
手持ちカードは中央下に表示したいので、アンカーを中央下に設定する
Color Node が Mouse Input を止めることがあるので注意
Color Node の Mouse Filter を Stop から Ignore に変更する
Godot の Debugger で、Misc タブで現在クリックしているコントロールノードの名前とかが分かる
Hand クラスを作成し、BaseState になったカードの位置を戻す処理を実装
BaseState になった時のシグナルを受信し、Hand ノードに reparent させる
DraggingState に入ったら 0.05 秒の Timer でフラグ変更猶予を設けて、UXを改善
https://www.youtube.com/watch?v=Mt6JaE1xn6w
part3
シングルターゲットのAttack カードの挙動実装
マウスカーソルを追従するラインが表示され、敵の範囲に置いて使用する
Resource を extends した、独自Card クラスを作成
code:gd
class_name Card
extends Resource
enum Type {ATTACK, SKILL, POWER}
enum Target {SELF, SINGLE_ENEMY, ALL_ENEMIES, EVERYONE}
@export_group("Card Attributes")
@export var id: String
@export var type: Type
@export var target: Target
Warrior キャラクター配下に、それぞれ攻撃カード・防御カードのリソースを作成する
CardUIノードに Card リソースを割り当て
Enemyノードを作成
敵用の新規Collisiton Layer を追加
Mask はカードレイヤーと
CardTargetSelector ノードを作成
小さい CollisitonShape2D の Area2Dを持つ
Layer は カードレイヤー、Mask は Enemyレイヤーと
Line2D ノードを作成
https://gyazo.com/4186fd53f8b2a036b4fc5ea3dc0ef10c
色や幅、カーブを調整
CardTargetSlector ノード配下に CanvasLayer を追加し、別レイヤーで定義
その配下に Line2Dノードを置く
Battleシーンに設置
どうやってカード操作を CardTargetSelector ノードに通知するか?
https://gyazo.com/cdc995edb56694578b105b605c109a09
AimingState を新規作成
enter 時に 演出処理や位置調整を行って、ain_started シグナルを emit
aimin state は、single_target のカードの時にだけ使う
https://gyazo.com/dbc6fc0e1f49fce49dc350eeebb10180
aiming してすぐに mouse を離して BASE 状態にすると、元の位置に戻らないバグ
これは、Tween アニメーション中であるため
BASE 状態に enter() 時に以下のようにして Tween を消すとよい
code:gd
if card_ui.tween and card_ui.tween.is_running():
card_ui.tween.kill()
https://www.youtube.com/watch?v=8MdvvNUrD3w
PART4 Stats with Resources
Godot の Resouces の特徴
https://gyazo.com/7bbdaf1293e3e878feb13ea74ee9f31a
CardPile Resource の作成
デッキのカスタムリソース
例えばドローカードメソッドの実装例
code:gd
func draw_card() -> Card:
var card = cards.pop_front()
card_pile_size_changed.emit(cards.size())
return card
debug 用に 文字列出力処理
code:gd
func _to_string() -> String:
var _card_strings: PackedStringArray = []
for i in range(cards.size()):
_card_strings.append("%s: $s" % [i+1, cardsi.id]) return "\n".join(_card_strings)
敵の stats 用カスタムリソースの作成
hp などを定義
敵を複数用意する時のために、duplicate() して instanceを生成する。そうしないと、HPなどが共有されてしまう
code:gd
func create_instance() -> Resource:
var instance: Stats = self.duplicate()
instance.health = max_health
instance.block = 0
return instance
キャラクターの stats 用カスタムリソースの作成
上で作成した敵の Stats を extends して作成する
初期デッキやカード数、マナ数などのステータスを定義する
code:gd
class_name CharacterStats
extends Stats
@export var starting_deck: CardPile
@export var cards_per_turn: int
@export var max_mana: int
var mana: int : set = set_mana
var deck: CardPile
var discard: CardPile
var draw_pile: CardPile
(省略)
func create_instance() -> Resource:
var instance: CharacterStats = self.duplicate()
instance.health = max_health
instance.block = 0
instance.reset_mana()
instance.deck = instance.starting_deck.duplicate() # ここ注意すること
instance.draw_pile = CardPile.new()
instance.discard = CardPile.new()
return instance
ブロックとHPのUIを作成
HBoxContaner を組み合わせて作成
https://gyazo.com/9dfad65d4b22b3dcf54adbbedecc07f6
Player.gd で、stats を読み込んでいる
ちょいちょい await ready をしている箇所がある。今後重要になりそうなので覚えておきたい
code:gd
if not is_inside_tree():
await ready
https://www.youtube.com/watch?v=JlKkH5F2hYE
PART5: Card Visuals + Logic, Mana Management
Enemy にターゲットをした時に Arrow が表示されるように
Card Resource に Viaual 関連のプロパティを追加
Texture のパス
説明文の string
code:gd
@export_group("Card Visuals")
@export var icon: Texture
@export_multiline var tooltip_text: String
CardUI ノードを修正
Panel ノードを追加して、FullRect
Theme Overrideds して、StlyeBoxFlat を適用
色と、ボーダーを設定
マナコスト用の Label ノードを追加
左上アンカー
アイコン用の TextureRect ノードを追加
中央アンカー
Ignore, Keep Centeter にして Size を調整
ホバー中及びドラッグ中のカードは見た目が分かるようにする
Panel の Theme を Make Unique して、別の色にした Theme を別名保存
Mana UI ノードを作成
Panel ノードの配下に Label ノードを作成
my_theme の Panel を編集する
Theme ウインドウで、Manage Items で Panel を選択し、Card UI で作成した既存のtheme設定を import する
ManaUI class の gdscript を追加
マナの変化を購読して Label を変更する処理
code:gd
@export var char_stats: CharacterStats : set = _set_char_stats
@onready var mana_label: Label = $ManaLabel
func _set_char_stats(value: CharacterStats) -> void:
char_stats = value
if not char_stats.stats_changed.is_connected(_on_stats_changed):
char_stats.stats_changed.connect(_on_stats_changed)
if not is_node_ready():
await ready
_on_stats_changed()
func _on_stats_changed() -> void:
省略
カードの効果を実装していく
https://gyazo.com/b839b03146b44a080f105db8f7047955
Effect クラスをどう管理するか
RefCounted を採用する
ベースとなるのは以下
code:gd
class_name Effect
extends RefCounted
func execute(_targets: ArrayNode) -> void: pass
これを継承して、例えば防御Effectを実装すると
code:block_effect.gd
class_name BlockEffect
extends Effect
var amount := 0
func execute(targets: ArrayNode) -> void: for taget in targets:
if not target:
continue
if target is Enemy or target is Player:
target.stars.block += amount
カードの使用に関するロジックの実装
Card リソースで、target enum に応じて target を取得できる処理追加
code:gd
func _get_targets(targets: ArrayNode) -> ArrayNode: if not targets:
return []
var tree := targets0.get_tree() match target:
Target.SELF:
return tree.get_node_in_gropu("player")
Target.ALL_ENEMIES:
return tree.get_node_in_gropu("enemies")
Target.EVERYONE:
return tree.get_node_in_gropu("player") + return tree.get_node_in_gropu("enemies")
_:
return [] # シングルターゲットは別途targetを取得する
実際にカードを使用するロジック
code:gd
func play(targets: ArrayNode, char_stats: CharacterStats) -> void: Events.card_played.emit(self)
char_stats.mana -= cost
if is_sigle_targeted():
apply_effects(targets)
else:
apply_effects(_get_targets(targets))
func apply_effects(_targets: ArrayNode) -> void: pass # これを各カードリソースがロジック実装する
これはめっちゃ参考になる
これまで用意したクラスを使って、例えば単体攻撃カードを実装
code:warrior_axe_attack:gd
extends Card
func apply_effects(targets: ArrayNode) -> void: var damage_effect := DamageEffect.new()
damage_effect.amount = 6
damage_effect.execute(targets)
このクラスを、既存の warrior_axe_attack.tres カスタムリソースに割り当てる(今は card.gd が割り当てられている)
カードの Released State になった時に、targets を取得して card.play を実行するロジックで組み立てる
プレイヤーのマナに応じて、カードが使用可能かどうかのフラグチェックを追加
使用不能の場合は見た目を灰色にする
https://www.youtube.com/watch?v=UAFpF0eJXXo
PART6: Card Tooltips, Drawing & Discarding
カードの説明を tooltip 表示
PanelContainer で見た目を調節
MarginContainer で上下左右のマージンを設定
VBoxContainer で Icon と RichLabelText を並べる
説明文は BBCode Enabled にして
Event Bus に ツールチップ表示関連の signal を追加
code:gd
signal card_tooltip_requested(card: Card)
signal tooltip_hide_requested
連続でツールチップを表示しようとすると、一瞬フェードアウトが入ってちらつく件の解消
フェードアウトの Tween に入る前にフラグを変更してからタイマー処理で待つ
timeout 後にフラグが変わってなければ Tween 処理を実行する
https://gyazo.com/ebe5897d730926b20733ab193c3ef495
BattleUIクラスを作成
各種UIを管理
Battle クラスを作成
これがメインのルートノードとなる
https://gyazo.com/6b633935322dd70bcd9ce046e9d529a8
PlayerHandler ノードを追加
GDScript を作成
code:player_handler.gd
func start_battle(char_stats: CharacterStatus) -> void:
character = char_stats
character.draw_pile = character.deck.duplicate(true)
character.draw_pile.shuffle()
character.discard = CardPile.new()
start_turn()
func start_turn() -> void:
character.block = 0
character.reset_mana()
draw_cards(character.cards_per_turn)
func draw_card() -> void:
hand_add_card(character.draw_pile.draw_card())
func draw_cards(amount: int) -> void:
var tween := create_tween()
for i in range(amount):
tween.tween_callbacl(draw_card)
tween.tween_interval(HAND_DRAW_INTERVAL)
tween.finished.connect(
func(): Events.player_hand_drawn.emit()
)
EndTurnButton を設置
各種状態の Button theme を設定
Discard 処理
draw_card 時に、draw_pile が空だったら discard_pile から入れ直してシャッフル
https://www.youtube.com/watch?v=Y2XLCzxYBRg
PART7: Enemy AI, Win/Lose States
EnemyStats を作成
exteds Stats
ai プロパティだけを持つ
EnemyActions
Conditional と Chance-Based
https://gyazo.com/95babc4ef73b449d4b7700158c615425
EnemyAction クラスを作成
code:gd
class_name EnemyAction
extends Node
enum Type {CONDITIONAL, CHANCE_BASED}
@export var type: Type
@export_range(0.0, 10.0) var chance_weight := 0.0
@onready var accumulated_weight := 0.0
var enemy: Enemy
var target: Node2D
func is_performable() -> bool:
return false
func perform_action() -> void:
pass
EnemyActionPicker シーンとGDScript を作成
Conditional な Action を取得できたらそれを返す
そうでない場合は、ChanceBased の Action から重み付けのランダム抽選をする
カニエネミーノードに上記のEnemyActionPicker Script をアタッチ
子ノードを3つ作成し、それぞれに EnemyAction GDScript を新規作成してアタッチする
AttackAction
BlockAction
MegaBlockAction
EnemyHandelr ノードと GDScript を作成
敵ターンを管理する
Battle GDScript のプロパティに EnemyHandler を追加
プレイヤーターンと敵ターンが交互に行われるようにシグナル管理する
https://www.youtube.com/watch?v=g4vcoY8X_1o
PART8: Enemy Intents & Polish (08/08)
これが最後ではなく、まだ動画は続く
Theme は毎回 Control ノードに割り当てるのではなく、Project Settings でデフォルトを設定しておいたほうが楽という指摘
Card の index 更新処理のバグについて修正
敵の攻撃予測: intent の作成
intent カスタムリソースの作成
number と icon だけを持つシンプルなリソース
EnemyAction のプロパティにこの intent を追加
アクションに紐づく情報
この intent については、いちいちファイルを作成したくない。シンプルな情報だから
インスペクタ上で New を選択して、その場で EnemyAction シーンに紐づける
https://gyazo.com/ec33eacdf630285d7c54d36e3e9aa18f
なるほどな〜。これは確かに便利だkidooom.icon
Intent UI を作成
演出を追加
やりたいこと
攻撃時に Shake と White Flash を追加する
プレイヤーがダメージを受けた時は画面全体を赤くする
shaker スクリプトをAutoload で作成
引数の object をshake する
ダメージ処理の中で tween を生成し、shake の後にダメージが発生して、interval 後に死亡判定させる
tween を時間差の処理順番だけに使うのもありなのかと参考になったkidooom.icon
白くする shader を sprite の material の割当
ダメージを受けた時にその material に割り当てる。その後にn ull にする
画面全体を赤くする
CanvasLayer を新規追加
赤色で透明度を設定
点滅のための Timer を追加
この CanvasLayer 配下の ColorRect が Mouse Input を Filter してしまうので、Ignore に設定
プレイヤーがダメージを受けたかどうかをどうチェックするか
Stats で行うと、プレイヤーだけに限定しない
CharacterStats で、オーバーライドした take_damage でsuper.take_dagame(damage) を実行しつつ、前後で helath が減っていたら signal を emit する
https://www.youtube.com/watch?v=GrDIg96Ames
PART9: SFX, Music & Adding New Content
音をつけていく
Audo bus を2つ追加
SFX
Music
SoundPlayer の AutoLoads クラスを作成
MusicPlayer という名前で Node を作成し、SoundPlayer をアタッチ
AudioStreamPlayer をいくつか追加
同時に再生できる音声の分だけ
同様に SFXPlayer Node を作成し、AudioStreamPlayer をいくつか追加
sound_player.gd をアタッチ
各カードリソースと、EnemyAction の sound をアサインして、play 処理を追加していく
バトル終了UIを作成する
Labelの設定に Label Settings というのが新しく追加されてるっぽい
敗北した後にもカードが配られるのはおかしいので、GAME を pause させる
ただし全部 pause すると、restart ボタンも押せなくなるので、ゲーム終了UIは stop しないようにインスペクタで設定する
音楽も止まらないように、Process Mode の設定を Always に変更する
script_templates を作成
既存のコードをコピペしなくてもいいように template 化して、新規script 作成時に利用できる
これは知らなかった!便利だから使っていこうkidooom.icon
template を利用して、新しい敵キャラ bat を作成